Aluno: Rafael Capaci Pereira
Professores: Luis Gustavo Nonato e Moacir Antonelli Ponti
Cemeai - ICMC/USP São Carlos
A avaliação vale 10 pontos. As questões de 1 a 4, caso respondidas da forma correta, já totalizam 10 pontos.
**ATENÇÃO:**
pip.!pip install tqdm
!pip install plotly
Requirement already satisfied: tqdm in /home/capaci/anaconda3/envs/mba/lib/python3.9/site-packages (4.59.0) Requirement already satisfied: plotly in /home/capaci/anaconda3/envs/mba/lib/python3.9/site-packages (4.14.3) Requirement already satisfied: six in /home/capaci/anaconda3/envs/mba/lib/python3.9/site-packages (from plotly) (1.16.0) Requirement already satisfied: retrying>=1.3.3 in /home/capaci/anaconda3/envs/mba/lib/python3.9/site-packages (from plotly) (1.3.3)
Considere o arquivo modcovid.pdf (disponível para download no moodle). Escreva um código para extrair o texto (ASCII) do arquivo PDF e escreva o texto extraído em um arquivo chamado modcovid.txt.
Para resolver a questão, foram desenvolvidas as funções convert_pdf_to_string e write_string_to_file.
O arquivo .pdf é lido como imagem e o conteúdo de todas as páginas é retornado como uma string.
from pdf2image import convert_from_path
from PIL import Image
from pytesseract import image_to_string
def convert_pdf_to_string(pdf_path):
"""
Dado o caminho de um arquivo em pdf, converte o arquivo para uma imagem e
concatena o texto de cada página e retorna esse texto.
"""
imgs = convert_from_path(pdf_path)
content = ''
for img in imgs:
content += image_to_string(img, lang='por')
return content
def write_string_to_file(content, file_path):
f = open(file_path, 'w')
f.write(content)
f.close()
content = convert_pdf_to_string(pdf_path='./modcovid.pdf')
write_string_to_file(content, file_path='modcovid.txt')
Leia o arquivo modcovid.txt e realize as seguinte operações:
Extraia todas palavras contidas no arquivo e armazene em uma lista de palavras (utilize o método word_tokenize do pacote nltk).
Remova da lista de palavras todas as "palavras" que não sejam formadas exclusivamente de caracteres do alfabeto.
Quantas palavras com apenas 1 caractere sobraram na lista?
from nltk import word_tokenize
A função get_tokens_from_file lê um arquivo e extrai as palavras do texto, retornando-as em uma lista através da função word_tokenize, do pacote nltk
def get_tokens_from_file(filename):
f = open(filename, 'r')
content = f.read()
f.close()
return word_tokenize(content, language='portuguese')
A função abaixo remove todas as palavras que não são formadas exclusivamente por caracteres alfanuméricos
def remove_non_alpha_from_tokens_list(tokens):
return [token for token in tokens if token.isalpha()]
A próxima função filtra as palavras de uma lista pelo tamanho
def filter_tokens_by_length(tokens, length):
return [token for token in tokens if len(token) == length]
A seguir, as três funções declaradas anteriormente são executadas, lendo o arquivo modcovid.txt, removendo as palavras não alfanuméricas e por fim, filtrando as palavras de tamanho 1, de modo a responder a terceira pergunta.
Ao executar esses passos, observa-se que o número de palavras de tamanho 1 é 44
tokens = get_tokens_from_file('modcovid.txt')
tokens = remove_non_alpha_from_tokens_list(tokens)
len(filter_tokens_by_length(tokens, length=1))
44
Carregue o arquivo artists_mba21.csv e armazene em um pandas DataFrame.
Esse arquivo possui os atributos:
import pandas as pd
%matplotlib inline
df = pd.read_csv('artists_mba21.csv')
Exiba um histograma contendo a frequência dos diferentes gêneros dos artistas. Note que um artista pode ter mais do que um gênero associado.
Dica: use a função str.split(',') para separar múltiplos gêneros
df.head()
| name | years | genre | nationality | bio | wikipedia | paintings | |
|---|---|---|---|---|---|---|---|
| 0 | Amedeo Modigliani | 1884 - 1920 | Expressionism | Italian | Amedeo Clemente Modigliani (Italian pronunciat... | http://en.wikipedia.org/wiki/Amedeo_Modigliani | 193 |
| 1 | Vasiliy Kandinskiy | 1866 - 1944 | Expressionism,Abstractionism | Russian | Wassily Wassilyevich Kandinsky (Russian: Васи́... | http://en.wikipedia.org/wiki/Wassily_Kandinsky | 88 |
| 2 | Diego Rivera | 1886 - 1957 | Social Realism,Muralism | Mexican | Diego María de la Concepción Juan Nepomuceno E... | http://en.wikipedia.org/wiki/Diego_Rivera | 70) |
| 3 | Claude Monet | 1840 - 1926 | Impressionism | French | Oscar-Claude Monet (; French: [klod mɔnɛ]; 14 ... | http://en.wikipedia.org/wiki/Claude_Monet | 73 |
| 4 | Rene Magritte | 1898 - 1967 | Surrealism,Impressionism | Belgian | René François Ghislain Magritte (French: [ʁəne... | http://en.wikipedia.org/wiki/René_Magritte | 194 |
Podemos verificar que na coluna genre, os dados estão dentro de uma string, onde os gêneros de um artista são separados por vírgula.
Inicialmente, o método str.split() é utilizado para converter cada string em um array contendo os gêneros.
df['genre'] = df['genre'].str.split(',')
Como estamos interessados somente na frequência dos gêneros, utilizaremos o método explode para "concatenar" todas as listas em uma nova série do pandas.
genres = df['genre'].explode()
A partir da série genres montamos o gráfico de barras com as frequências de cada gênero, ordenadas de maneira decrescente.
import plotly.graph_objects as go
counts = genres.value_counts(ascending=True)
bar_plot = go.Bar(x=counts,
y=counts.index,
orientation='h',
)
fig = go.Figure(bar_plot)
fig.update_layout(title='Frequência por gênero',
height=800,
yaxis={'title': 'Gêneros'},
xaxis={'title': 'Frequência'},
)
fig.show()
Trate a coluna "paintings", a qual deveria conter apenas atributos numéricos inteiros.
Siga os seguintes passos:
Após o tratamento exiba a estatística descritiva da coluna tratada usando describe()
Dica: há várias soluções possíveis, mas usar expressões regulares pode ser útil nesse caso. Por exemplo a expressão '(\d+)' extrai apenas dígitos
Primeiramente, vamos verificar quais são os valores possíveis da coluna, para entender se há muitos valores errôneos
df['paintings'].values
array(['193', '88', '70)', '73', '194', '139', '90', '99', '877', '"117"',
'137', '126', '171', '439', '141', '336', '291', '120', '87',
'328', '259', '134', '239', '119', '164', '55', '143', '128',
'186', '81', '702', '262', '255', '81', '59', '91', '66', '67',
'47', '31', '70', '43', '188', "'84'", '102', '181', '311', '109',
'49', '24', 'three', "'25'", '29'], dtype=object)
Podemos observar que praticamente todos os valores são formados por números, com poucos erros, às vezes alguns caracteres junto com os valores numéricos, como parênteses ou aspas. Além desse tipo de valor, existe o valor "three", que é o número 3 por extenso em inglês. Como é um caso único, podemos corrigir esse valor diretamente, mantendo-o como uma string, para corresponder com o tipo dos outros valores:
df[df['paintings'] == 'three'] = '3'
Agora podemos tratar os outros casos.
Primeiramente, vamos extrair os valores numéricos de cada linha da nossa coluna, utilizando o método extract com a expressão regular (\d+)
df['paintings'] = df['paintings'].str.extract('(\d+)')
Observando novamente os valores da coluna, é possível ver que temos somente os valores numéricos, porém ainda como strings
df['paintings'].values
array(['193', '88', '70', '73', '194', '139', '90', '99', '877', '117',
'137', '126', '171', '439', '141', '336', '291', '120', '87',
'328', '259', '134', '239', '119', '164', '55', '143', '128',
'186', '81', '702', '262', '255', '81', '59', '91', '66', '67',
'47', '31', '70', '43', '188', '84', '102', '181', '311', '109',
'49', '24', '3', '25', '29'], dtype=object)
Então convertemos esses valores para numéricos, de forma que seja possível avaliar as estatisticas descritivas da coluna
df['paintings'] = pd.to_numeric(df['paintings'], errors='coerce')
df['paintings'].describe()
count 53.000000 mean 160.433962 std 156.816589 min 3.000000 25% 70.000000 50% 119.000000 75% 188.000000 max 877.000000 Name: paintings, dtype: float64
Dada uma imagem query.jpg de uma pintura da qual não sabemos o artista, gostaríamos de fazer uma busca numa base de dados e recuperar obras similares de acordo com suas cores. Para isso utilizaremos um descritor baseado em cores conhecido por Border-Interior Classification (BIC)
Stehling, R. O., Nascimento, M. A., & Falcão, A. X. (2002). A compact and efficient image retrieval approach based on border/interior pixel classification. In Proceedings of the eleventh international conference on Information and knowledge management (pp. 102-109).
O BIC funciona da seguinte forma:
Exemplo seja a matriz abaixo uma imagem com $C=4$, ou seja, com cores 0, 1, 2 e 3:
| 3 | 3 | 3 | 2 | 2 | 0 |
| 3 | 3 | 3 | 2 | 2 | 2 |
| 3 | 3 | 3 | 2 | 2 | 2 |
| 3 | 3 | 3 | 3 | 2 | 2 |
| 0 | 3 | 3 | 1 | 2 | 0 |
Os valores em negrito correspondem à cores "interior" e o restante de "borda".
Assim, teríamos o seguintes histogramas:
h_borda = [3,1,9,11]
h_inter = [0,0,2,4]
Note que cada vetor representa as frequências, em ordem, das cores 0, 1, 2 e 3. Por exemplo, há 3 pixels da cor 0 referentes à borda, há 2 pixels da cor 2 referentes a interior.
Após computar os histogramas, é preciso normalizá-los para evitar problemas com imagens de diferentes resoluções. Diferentes métodos podem ser utilizados. Aqui utilizaremos a normalização L1, dividindo os elementos pela soma absoluta do vetor, resultando em:
h_borda_nr = [0.125, 0.0417, 0.375, 0.4583]
h_inter_nr = [0.000, 0.0000, 0.333, 0.6667]
Assim, para uma determinada imagem com $C$ cores, o vetor de características terá $2\cdot C$ dimensões, relativas aos 2 histogramas concatenados. Após a concatenação normalizamos novamente:
bic_norm = [0.0625, 0.0208, 0.1875, 0.2292, 0.0000, 0.0000, 0.1667, 0.3333]
Esse será o vetor de características final a ser utilizado
Use o descritor BIC com $C=32$ para as imagens disponibilizadas. Use a função disponibilizada rescale_image_colors() para reescalar a imagem.
Faça uma busca no diretório paintings21, retornando as 5 imagens mais similares a query.jpg de acordo com o descritor BIC e a distância Euclidiana. Exiba a imagem de consulta, e também as 5 imagens retornadas, com seus nomes e valores da distância obtidos.
# inclua aqui as bibliotecas e funções
# inclua os pacotes necessários e as funções necessárias
from operator import itemgetter
from os import listdir
import imageio
import matplotlib.pyplot as plt
import numpy as np
from skimage import feature
def rescale_image_colors(img, n_colors=32):
# RGB para escala de cinza usando Luma Rec.709
img = np.array(img, dtype=np.float64, copy=False)
if (len(img.shape) > 2):
img = img[:, :, 0] * 0.2126 + img[:, :, 1] * 0.7152 + img[:, :, 2] * 0.0772
# reescalando numero de cores
img = img/255.0
img = (img*(n_colors-1)).astype(np.uint8)
return img
Inicialmente, é definida a função bic juntamente com algumas funções auxiliares de modo a implementar o algoritmo BIC, especificado acima.
def bic(img, n_colors=32):
"""
Implementação do descritor bic, como enunciado anteriormente
"""
img = rescale_image_colors(img, n_colors=n_colors)
h_borda = np.zeros(n_colors)
h_inter = np.zeros(n_colors)
for i in range(0, img.shape[0]):
for j in range(0, img.shape[1]):
current = img[i, j]
is_img_border = _is_img_border(i, j, img.shape)
if is_img_border or _is_color_border(img, i, j, current):
h_borda[current] += 1
else:
h_inter[current] += 1
h_borda = h_borda/h_borda.sum()
h_inter = h_inter/h_inter.sum()
return np.concatenate((h_borda, h_inter))
def _is_img_border(i, j, shape):
return i == 0 or j == 0 or i == shape[0] -1 or j == shape[1] -1
def _is_color_border(img, i, j, current):
if img[i - 1, j] != current:
return True
if img[i + 1, j] != current:
return True
if img[i, j - 1 ] != current:
return True
if img[i, j-1] != current:
return True
return False
Em seguida, vamos ler a imagem de consulta e guardá-la no dicionário query_image.
Todas as imagens serão guardadas num dicionário com a mesma estrutura, contendo as seguintes chaves:
name: o nome da imagemimg: o objeto da imagem, retornado pelo método imread da biblioteca imageioimgQ = imageio.imread("paintings21/query.jpg")
query_image = {'name': 'Imagem de consulta', 'img': imgQ}
As características da imagem, extraídas pela função bic, serão armazenada na variável query_features para, futuramente, ser comparada com as outras imagens
query_features = bic(query_image['img'])
Agora vamos ler todas as outras imagens do diretório e armazená-las em uma estrutura idêntica a da imagem de consulta, dentro da lista images.
Aqui as características das imagens também são extraídas, porém, serão armazenadas na variável images_features, que mais tarde será convertida para uma matriz do numpy. Armazená-las nessa estrutura nos permite calcular as distâncias de maneira vetorizada, otimizando o código.
from tqdm import tqdm
l_imgs = listdir('./paintings21/')
l_imgs.remove('query.jpg')
images_features = []
images = []
for i, im in enumerate(tqdm(l_imgs)):
imgS = imageio.imread('./paintings21/'+im)
features = bic(imgS)
img = {'name': im, 'img': imgS}
images.append(img)
images_features.append(features)
100%|██████████| 40/40 [00:10<00:00, 3.98it/s]
images_features = np.array(images_features)
A função k_nearest_indexes_and_distances calcula a distância euclidiana e retorna os k indices das imagens mais próximas, bem como as respectivas distâncias.
def k_nearest_indexes_and_distances(x_train, xi_test, k):
"""
Função que retorna k indices dos objetos mais proximos do objeto alvo.
Inicialmente essa função foi implementada no desafio de implementação
do KNN das disciplinas de ICD e PCD, para retornar as classes mais próximas.
Aqui ela sofreu uma pequena alteração e a implementação original está
disponível em: https://github.com/capaci/capaci-kit-learn/blob/main/src/knn.py#L51
"""
distances = _euclidean_distance(x_train, xi_test)
k_nearest_indexes = distances.argpartition(k)[:k]
return k_nearest_indexes, distances[k_nearest_indexes]
def _euclidean_distance(images, image_query):
"""
Função que calcula a distância euclidiana de maneira vetorizada.
Inicialmente essa função foi escrita para o desafio de implementação
do KNN nas disciplinas de ICD e PCD, disponível em:
https://github.com/capaci/capaci-kit-learn/blob/main/src/knn.py#L57
"""
squared = (image_query - images) ** 2
squared_sum = np.sum(squared, axis=1)
distances = np.sqrt(squared_sum)
return distances
k_nearest_indexes, distances = k_nearest_indexes_and_distances(images_features, query_features, k=5)
As imagens mais próximas são recuperadas a partir do seu índice e armazenadas na variável nearest_images
nearest_images = itemgetter(*k_nearest_indexes)(images)
A seguir, as imagens são exibidas. A primeira é a imagem de consulta e as seguintes são as mais próximas, em ordem.
A imagem de consulta é concatenada no início das imagens mais próximas, bem como o valor 0 é concatenado as distâncias, permitindo que todas as imagens sejam exibidas no mesmo laço.
rows, cols = 2, 3
fig, axs = plt.subplots(rows, cols)
all_images = [query_image] + list(nearest_images)
_distances = [0] + list(distances)
fig.set_size_inches(13, 6)
for img, distance, ax in zip(all_images, _distances, axs.flat):
title = f'{img["name"]}'
ax.title.set_text(title)
if distance != 0:
distance_text = f'distancia = {distance:.3f}'
ax.text(0.5, -0.1, distance_text, ha="center", transform=ax.transAxes)
ax.axis('off')
ax.imshow(img['img'])
fig.tight_layout()
plt.show()